﻿//--------------------------------------------------------------------------
// 
//  Copyright (c) Chili Software.  All rights reserved. 
// 
//  File: ObjectListHolder_1.cs
//
//  Description: Holds a list of related persistent objects.
// 
//--------------------------------------------------------------------------

using System;
using System.Reflection;
using System.Collections.Generic;
using System.Runtime.Serialization;
using System.Xml.Serialization;
using Chili.Opf3.Query;
using System.Collections;

namespace Chili.Opf3.Relations
{
	/// <summary>
	/// Holds a list of objects that are connected with the parent object.
	/// </summary>
	/// <remarks>
	/// This class is used in a persistent class to hold a list of objects that are connected with 
	/// the parent object. When accessing the first time the 
	/// <see cref="Opf3.Relations.ObjectHolder{T}.InnerObject">InnerObject</see> property the related
	/// objects are loaded. This is called delayed loading and reduces roundtrips to the storage.
	/// </remarks>
	/// <example>
	/// The following example shows how to use the ObjectListHolder in your persistent objects. In the
	/// example each User has associated Calls.
	/// <code lang="CS">
	/// [Persistent("USER", PoolSize = 50)]
	/// public class User
	/// {
	///		// The class contains an ObjectListHolder and a RelationAttribute
	///		// that specifies the relation between the parent object and the 
	///		// related objects. It demonstrates also how to use a condition to 
	///		// delimit the items in the list and to sort the result.
	///		[Relation("Id", "UserId")]
	///		private ObjectListHolder&lt;Call&gt; _calls = 
	///			new ObjectListHolder&lt;Call&gt;(new List&lt;Call&gt;(), "Duration &lt; {0} SortBy Duration", "50");
	///	
	///		public List&lt;Call&gt; Calls
	///		{
	///			get { return _calls.InnerObject; }
	///			set { _calls.InnerObject = value; }
	///		}
	///		
	///		// ... Other properties and code.
	/// }
	/// </code>
	/// </example>
	[Serializable]
	public sealed class ObjectListHolder<T> : ObjectHolderBase
	{
		/// <summary>
		/// Creates a new instance of the <see cref="ObjectListHolder{T}">ObjectListHolder Class</see>.
		/// </summary>
		/// <param name="list">List that is populated with the connected objects.</param>
		public ObjectListHolder(IList<T> list) : base()
		{
			if (list == null)
				throw new ArgumentNullException("list");
			this.ManagedObject = list;
		}

		/// <summary>
		/// Creates a new instance of the <see cref="ObjectListHolder{T}">ObjectListHolder Class</see>.
		/// </summary>
		/// <param name="list">List that is populated with the connected objects.</param>
		/// <param name="conditions">OPath that is used when retrieving the objects. For more information about OPath
		/// look at the <see cref="Opf3.Query.ObjectQuery{T}">ObjectQuery</see> class.</param>
		/// <param name="parameters">A list of parameters that are used in the OPath expression.</param>
		public ObjectListHolder(IList<T> list, string conditions, params object[] parameters)
			: base(new ObjectQuery<T>(conditions, parameters))
		{
			if (list == null)
				throw new ArgumentNullException("list");
			this.ManagedObject = list;
		}

		/// <summary>
		/// Creates a new instance of the <see cref="ObjectListHolder{T}">ObjectListHolder Class</see>.
		/// </summary>
		/// <param name="list">List that is populated with the connected objects.</param>
		/// <param name="query"><see cref="Opf3.Query.IQuery">IQuery</see> that is executed to load the associated 
		/// objects. This parameter allows you to use any kind of query without using the build in functionality.</param>
		public ObjectListHolder(IList<T> list, IQuery query) : base(query)
		{
			if (list == null)
				throw new ArgumentNullException("list");
			this.ManagedObject = list;
		}

		/// <summary>
		/// Creates a new instance of the <see cref="ObjectListHolder{T}">ObjectListHolder Class</see>.
		/// </summary>
		/// <param name="info"></param>
		/// <param name="context"></param>
		private ObjectListHolder(SerializationInfo info, StreamingContext context) : base(info, context)
		{
		}

		/// <summary>
		/// Gets and sets the list of objects that are related to the parent object.
		/// </summary>
		/// <value>A <see cref="System.Collections.Generic.IList{T}">IList</see> that contains the 
		/// related persistent objects.</value>
		[XmlIgnore]
		public IList<T> Value
		{
			get { return this.InnerObject; }
			set { this.InnerObject = value; }
		}

		/// <summary>
		/// Depreciated: Use the Value property instead!
		/// Gets and sets the list of objects that are related to the parent object.
		/// </summary>
		/// <value>A <see cref="System.Collections.Generic.IList{T}">IList</see> that contains the 
		/// related persistent objects.</value>
		[XmlIgnore]
		public IList<T> InnerObject
		{
			get
			{
				if (!AutomaticLoad)
					return (IList<T>)this.ManagedObject;

				// Get the ObjectSet if not loaded.
				if (!IsLoaded)
				{
					InnerLoad(this.ObjectContext);

					Tracing.Current.TraceStorageIO("Loaded persistent's related objects from storage.");
					// Set the ObjectSetHolder as loaded.
					IsLoaded = true;
				}

				return (IList<T>)this.ManagedObject;
			}
			set 
			{
				if (value == null)
					throw new ArgumentNullException("value");

				this.ManagedObject = value;
				// Set the ObjectListHolder as loaded because the objects are set.
				if (((IList<T>)this.ManagedObject).Count > 0)
					IsLoaded = true;
			}
		}

		/// <summary>
		/// Loads the related object(s) from the storage.
		/// </summary>
		protected override void InnerLoad(ObjectContext context)
		{
			// Check if the parent implements the IRelationsPopulateHelper.
			IRelationsPopulateHelper helper = this.Parent as IRelationsPopulateHelper;
			// Check if the relation is handled by the parent.
			if (helper != null)
			{
				// Create the relation information.
				RelationInformation info = new RelationInformation(this.Parent, typeof(T), this.RelationAttribute, context);
				if (helper.RelationIsHandled(this.RelationAttribute.Name, info))
				{
					// Get the object from the parent if requested.
					this.ManagedObject = helper.GetRelatedObject(this.RelationAttribute.Name, info);
					return;
				}
			}

			// Creates the query.
			IQuery query = ObjectHolderUtility.PrepareQuery<T>(this.RelationAttribute, this.Query, this.Parent);
			if (query != null && context != null && this.ManagedObject != null)
			{
				IList<T> list = (IList<T>)this.ManagedObject;
				list.Clear();
				context.GetObjectList<T>(list, query);
			}
		}

		/// <summary>
		/// This method is invoked to persist the changes on the objects in the ObjectHolder.
		/// </summary>
		/// <param name="context"><see cref="Opf3.ObjectContext">ObjectContext</see> used to persist 
		/// the changes. It is the same <see cref="Opf3.ObjectContext">ObjectContext</see> as used by the
		/// parent object.</param>
		/// <param name="persistDepth">Specifies the depth of the loaded object graph that is persisted.</param>
		internal override void PersistChanges(ObjectContext context, PersistDepths persistDepth)
		{
			this.ObjectContext = context;

			// Persist the object using the internal context.
			if (this.ManagedObject == null)
				return;

			// Return if the persist depth is flat.
			if (PersistDepths.Flat == persistDepth)
				return;

			bool isMany2Many = (this.RelationAttribute.WeakPersistent != null);
			if (isMany2Many)
				ObjectHolderUtility.DeleteManyToMany<T>((IList<T>)this.ManagedObject, this.RelationAttribute, this.Parent, context);

			IList<T> list = (IList<T>)this.ManagedObject;
			// Loop over all persistents in the list and persist them.
			foreach (T persistent in list)
			{
				// Set to flat since there is only one level left.
				if (persistDepth == PersistDepths.FirstLevel)
					context.PersistChanges(persistent, PersistDepths.Flat);
				else
					context.PersistChanges(persistent, PersistDepths.FullObjectGraph);
			}

			// Save only if the parent object has not been deleted.
			if (isMany2Many && context.GetObjectInfo(Parent).ObjectState != ObjectStates.Deleted)
				ObjectHolderUtility.SaveManyToMany<T>(list, this.RelationAttribute, this.Parent, context);
		}

		/// <summary>
		/// This method connects the foreign-key properties in the child persistent objects with the identifier
		/// properties of the parent object.
		/// </summary>
		internal override void ConnectPropertiesInChildObjects()
		{
			if (this.ManagedObject == null)
				return;

			IList<T> list = (IList<T>)this.ManagedObject;
			foreach (T obj in list)
			{
				// Updates the connected property.
				UpdateConnectedProperty(obj);
			}
		}

		/// <summary>
		/// This method is invoked to check if any of the ObjectHolder's objects have been modified.
		/// </summary>
		/// <param name="context"><see cref="Opf3.ObjectContext">ObjectContext</see> used to check for modified 
		/// objects. It is the same <see cref="Opf3.ObjectContext">ObjectContext</see> as used by the
		/// parent object.</param>
		/// <returns>True if an object has been modified; false othewise.</returns>
		internal override bool HasModifiedObjects(ObjectContext context)
		{
			if (this.ManagedObject == null)
				return false;

			IList<T> list = (IList<T>)this.ManagedObject;
			// loop over all objects and check them for being modified.
			foreach (T obj in list)
			{
				if (context.IsModified(obj, true))
					return true;
			}

			return false;
		}

		/// <summary>
		/// This method is invoked to check if the <see cref="ObjectListHolder{T}" /> has removed objects.
		/// </summary>
		/// <returns>True if an object has been removed; false otherwise</returns>
		internal override bool HasRemovedObjects()
		{
			ObjectSet<T> objectSet = this.ManagedObject as ObjectSet<T>;
			if (objectSet != null)
				return (objectSet.RemovedList.Count > 0);
			
			return false;
		}

		/// <summary>
		/// <para>
		/// Invalidates the current instance. The next request of the InnerObject property reloads all the 
		/// related objects. This is only true if the automatic load of the related objects has been enabled. 
		/// Otherwise the reload method needs to be invoked (after calling Invalidate) to do a manual reload.
		/// </para>
		/// <para>
		/// The call of this method removes the objects also from the identity map if one is in place. This makes
		/// sure that the objects are really reloaded from the storage and not from the identity map again.
		/// </para>
		/// </summary>
		public override void Invalidate()
		{
			// clear the inner list.
			IList<T> list = (IList<T>)this.ManagedObject;
			list.Clear();
			base.Invalidate();
		}

		/// <summary>
		/// Gets the enumerator for the given loaded related data. This method doesn't load
		/// data from the storage but rather returns the currently loaded data.
		/// </summary>
		internal override IEnumerable GetRawEnumerator()
		{
			if (this.ManagedObject == null)
				yield break;

			IList<T> list = (IList<T>)this.ManagedObject;
			foreach (object obj in list)
			{
				yield return obj;
			}
		}
	}
}